﻿using System;
using System.Collections.Generic;
using System.Linq;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis
{
    public class HalsteadCache
    {
        public static readonly ISet<OperationCode> OperatorInstructions = new HashSet<OperationCode> { OperationCode.And, OperationCode.Or, OperationCode.Mul, OperationCode.Mul_Ovf,
            OperationCode.Div, OperationCode.Mul_Ovf_Un, OperationCode.Div_Un, OperationCode.Add, OperationCode.Add_Ovf, OperationCode.Add_Ovf_Un, OperationCode.Sub, OperationCode.Sub_Ovf, OperationCode.Sub_Ovf_Un,
            OperationCode.Xor, OperationCode.And, OperationCode.Or, OperationCode.Br_S, OperationCode.Brfalse_S,
            OperationCode.Br, OperationCode.Brfalse, OperationCode.Brtrue, OperationCode.Brtrue_S, OperationCode._Catch, OperationCode.Leave, OperationCode.Leave_S, OperationCode.Bne_Un, OperationCode.Bne_Un_S,
            OperationCode.Call, OperationCode.Throw, OperationCode.Endfinally, OperationCode.Sizeof, OperationCode.Rethrow,
            OperationCode.Newobj, OperationCode.Ldftn,
            OperationCode.Tail_, OperationCode.Castclass,
            OperationCode._Catch, OperationCode.Newarr, OperationCode.Box, OperationCode.Unbox, OperationCode.Unbox_Any,
            OperationCode.Isinst, OperationCode.Initobj, OperationCode.Ldtoken,
            OperationCode.Break
        };

        private readonly object _lockCache = new object();

        private readonly IDictionary<IDescriptor, HalsteadEntry> _cache = new Dictionary<IDescriptor, HalsteadEntry>();

        public IDictionary<IDescriptor, HalsteadEntry> Cache
        {
            get { return _cache; }
        }

        public HalsteadEntry GetCacheEntry(IDescriptor descriptor, IDependencyModel model)
        {
            var instructionCache = model.InstructionCache;
            lock (_lockCache)
            {
                if (_cache.ContainsKey(descriptor))
                {
                    return _cache[descriptor];
                }
                HalsteadEntry entry;
                if (descriptor is MemberDescriptor)
                {
                    entry = CreateMemberEntry((MemberDescriptor)descriptor, instructionCache, model);
                }
                else if (descriptor is TypeDescriptor)
                {
                    entry = CreateEntryForTypes(new[] { (TypeDescriptor)descriptor }, instructionCache, model);
                }
                else if (descriptor is SubmoduleDescriptor)
                {
                    entry = CreateSubmoduleEntry((SubmoduleDescriptor)descriptor, model);
                }
                else if (descriptor is ModuleDescriptor)
                {
                    entry = CreateModuleEntry((ModuleDescriptor)descriptor, model);
                }
                else if (descriptor is ProjectDescriptor)
                {
                    var project = (ProjectDescriptor)descriptor;
                    var relevantTypes = new List<TypeDescriptor>();
                    if (project.Modules != null)
                    {
                        foreach (var module in project.Modules)
                        {
                            var submodules = module.Submodules.ToArray();

                            foreach (var submodule in submodules)
                            {
                                relevantTypes.AddRange(from x in submodule.Types
                                                       where
                                                           x.AbstractionType != AbstractionType.Interface &&
                                                           x.AbstractionType != AbstractionType.Descriptive
                                                       select x);
                            }
                        }
                    }

                    return CreateEntryForTypes(relevantTypes, model.InstructionCache, model);
                }
                else
                {
                    throw new ArgumentException("descriptor");
                }
                _cache[descriptor] = entry;
                return entry;
            }
        }

        private HalsteadEntry CreateModuleEntry(ModuleDescriptor module, IDependencyModel model)
        {
            var submodules = module.Submodules.ToArray();
            var relevantTypes = new List<TypeDescriptor>();
            foreach (var submodule in submodules)
            {
                relevantTypes.AddRange(from x in submodule.Types
                                       where
                                           x.AbstractionType != AbstractionType.Interface &&
                                           x.AbstractionType != AbstractionType.Descriptive
                                       select x);
            }
            return CreateEntryForTypes(relevantTypes, model.InstructionCache, model);
        }

        private HalsteadEntry CreateSubmoduleEntry(SubmoduleDescriptor submodule, IDependencyModel model)
        {
            var relevantTypes = (from x in submodule.Types
                                 where x.AbstractionType != AbstractionType.Descriptive
                                 select x).ToArray();

            return CreateEntryForTypes(relevantTypes, model.InstructionCache, model);
        }

        private HalsteadEntry CreateEntryForTypes(IList<TypeDescriptor> types, InstructionCache instructionCache,
            IDependencyModel model)
        {
            var typeEntry = new HalsteadEntry();
            var accessedDescriptors = new HashSet<IDescriptor>();
            var distinctOperators = new HashSet<OperationCode>();
            var dummyMember = new MemberDescriptor();
            bool extends = false;
            bool hasNamespace = false;
            int countVariables = 0;

            foreach (var type in types)
            {
                var relevantMembers = (from x in type.Members
                                       where x.MemberType != MemberType.TypeReference
                                       select x).ToArray();

                dummyMember.Visibility |= type.Visibility;
                if (type.AbstractionType == AbstractionType.Static)
                {
                    dummyMember.IsStatic = true;
                }
                if (type.AbstractionType == AbstractionType.Abstract)
                {
                    dummyMember.IsAbstract = true;
                }

                typeEntry.TotalOperators += CountAccessors(type);

                int numberOfInheritedTypes = type.InheritedTypes == null ? 0 : type.InheritedTypes.Count;
                bool thisExtends = (numberOfInheritedTypes > 0);
                if (thisExtends)
                {
                    typeEntry.TotalOperators += 1;
                    foreach (var inherited in type.InheritedTypes)
                    {
                        typeEntry.TotalOperands += 1;
                        if (!accessedDescriptors.Contains(inherited))
                        {
                            accessedDescriptors.Add(inherited);
                        }
                    }
                }
                extends |= thisExtends;

                bool thisHasNamespace = type.Submodule != null && string.IsNullOrEmpty(type.Submodule.Name);
                typeEntry.TotalOperators += (thisHasNamespace ? 1 : 0);
                hasNamespace |= thisHasNamespace;

                foreach (var member in relevantMembers)
                {
                    var memberEntry = GetCacheEntry(member, model);
                    typeEntry.TotalOperands += memberEntry.TotalOperands;
                    typeEntry.TotalOperators += memberEntry.TotalOperators;

                    if (model.DependenciesPerMemberOut.ContainsKey(member))
                    {
                        model.DependenciesPerMemberOut[member]
                           .Where(x => !accessedDescriptors.Contains(x.Target)).ToList()
                           .ForEach(x => accessedDescriptors.Add(x.Target));
                    }

                    dummyMember.Visibility |= member.Visibility;
                    dummyMember.IsStatic |= member.IsStatic;
                    dummyMember.IsAbstract |= member.IsAbstract;
                    dummyMember.IsOverride |= member.IsOverride;

                    countVariables += member.VariableCount;

                    var instructions = instructionCache.GetInstructions(member);
                    foreach (var instruction in instructions)
                    {
                        var opCode = instruction.OpCode;
                        if (opCode == OperationCode.Calli || opCode == OperationCode.Callvirt)
                        {
                            opCode = OperationCode.Call;
                        }
                        else if (opCode == OperationCode.Ldvirtftn)
                        {
                            opCode = OperationCode.Ldftn;
                        }
                        if (OperatorInstructions.Contains(opCode) ||
                            opCode == OperationCode.Ret && !member.Signature.StartsWith("Void"))
                        {
                            if (!distinctOperators.Contains(opCode))
                            {
                                distinctOperators.Add(opCode);
                            }
                        }
                    }
                }
            }

            var typeReferences = (from x in accessedDescriptors
                                  where x is MemberDescriptor &&
                                        ((MemberDescriptor)x).MemberType == MemberType.TypeReference
                                  select (MemberDescriptor)x).ToArray();
            foreach (var type in types)
            {
                if (type.Submodule != null)
                {
                    typeEntry.TotalOperands++;
                    if (!accessedDescriptors.Contains(type.Submodule))
                    {
                        accessedDescriptors.Add(type.Submodule);
                    }
                }
                typeEntry.TotalOperands++;
                if (!accessedDescriptors.Contains(type) && !typeReferences.Any(x => Equals(x.ImplementingType, type)))
                {
                    accessedDescriptors.Add(type);
                }
            }

            typeEntry.DistinctOperators = distinctOperators.Count + CountAccessors(dummyMember)
                + (extends ? 1 : 0)
                + (hasNamespace ? 1 : 0);
            typeEntry.DistinctOperands = accessedDescriptors.Count + countVariables;
            return typeEntry;
        }

        private HalsteadEntry CreateMemberEntry(MemberDescriptor member, InstructionCache instructionCache, IDependencyModel model)
        {
            var entry = new HalsteadEntry();
            if (member.MemberType == MemberType.TypeReference ||
                member.ImplementingType.AbstractionType == AbstractionType.Descriptive)
            {
                return entry;
            }
            var distinctOperators = new HashSet<OperationCode>();
            int operands = 0;
            int operators = 0;
            var instructions = instructionCache.GetInstructions(member);
            var accessedMembers = 0;
            if (model.DependenciesPerMemberOut.ContainsKey(member))
            {
                accessedMembers = model.DependenciesPerMemberOut[member].Count;
            }
            foreach (var instruction in instructions)
            {
                var opCode = instruction.OpCode;
                if (opCode == OperationCode.Calli || opCode == OperationCode.Callvirt)
                {
                    opCode = OperationCode.Call;
                }
                else if (opCode == OperationCode.Ldvirtftn)
                {
                    opCode = OperationCode.Ldftn;
                }
                if (OperatorInstructions.Contains(opCode) ||
                    opCode == OperationCode.Ret && !member.Signature.StartsWith("Void"))
                {
                    operators++;
                    if (!distinctOperators.Contains(opCode))
                    {
                        distinctOperators.Add(opCode);
                    }
                }
                if (InstructionCache.FieldOpCodes.Contains(opCode) ||
                    InstructionCache.MethodOpCodes.Contains(opCode) ||
                    InstructionCache.LocalOpCodes.Contains(opCode))
                {
                    operands++;
                }
            }
            int accessorCount = CountAccessors(member);
            entry.TotalOperators = operators + accessorCount;
            entry.TotalOperands = operands;
            entry.DistinctOperators = distinctOperators.Count + accessorCount;
            entry.DistinctOperands = member.VariableCount + accessedMembers;
            return entry;
        }

        private int CountAccessors(MemberDescriptor member)
        {
            int count = 0;
            if (member.IsAbstract)
            {
                count++;
            }
            if (member.IsStatic)
            {
                count++;
            }
            if (member.IsOverride)
            {
                count++;
            }
            if ((member.Visibility & NodeVisibility.Private) == NodeVisibility.Private)
            {
                count++;
            }
            if ((member.Visibility & NodeVisibility.Public) == NodeVisibility.Public)
            {
                count++;
            }
            if ((member.Visibility & NodeVisibility.Protected) == NodeVisibility.Protected)
            {
                count++;
            }
            if ((member.Visibility & NodeVisibility.Internal) == NodeVisibility.Internal)
            {
                count++;
            }
            return count;
        }

        private int CountAccessors(TypeDescriptor type)
        {
            int count = 0;
            if ((type.AbstractionType & AbstractionType.Abstract) == AbstractionType.Abstract)
            {
                count++;
            }
            if ((type.AbstractionType & AbstractionType.Static) == AbstractionType.Static)
            {
                count++;
            }
            if ((type.AbstractionType & AbstractionType.Interface) == AbstractionType.Interface)
            {
                count++;
            }
            if ((type.Visibility & NodeVisibility.Private) == NodeVisibility.Private)
            {
                count++;
            }
            if ((type.Visibility & NodeVisibility.Public) == NodeVisibility.Public)
            {
                count++;
            }
            if ((type.Visibility & NodeVisibility.Protected) == NodeVisibility.Protected)
            {
                count++;
            }
            if ((type.Visibility & NodeVisibility.Internal) == NodeVisibility.Internal)
            {
                count++;
            }
            return count;
        }
    }
}